﻿/*
 * Copyright (c) 2008 Håvard Stranden
 * 
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 * 
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 */

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.Serialization;

namespace NTasking.Internals
{
    /// <summary>
    /// This class defines all the extension methods provided by the Copyable framework 
    /// on the <see cref="System.Object"/> type.
    /// </summary>
    public static class DeepCopyExtensions
    {

        /// <summary>
        /// This class keeps track of objects which have lrady been loned in order to
        /// make sure that the same object appearing several times in the source object
        /// graph is cloned only once in the target hierarchy
        /// </summary>
        class VisitedGraph : Dictionary<object, object>
        {

            /// <summary>
            /// Determines whether the specified key contains key.
            /// </summary>
            /// <param name="key">The key.</param>
            /// <returns>
            /// 	<c>true</c> if the specified key contains key; otherwise, <c>false</c>.
            /// </returns>
            public new bool ContainsKey(object key)
            {
                return key == null || base.ContainsKey(key);
            }

            /// <summary>
            /// Gets the <see cref="System.Object"/> with the specified key.
            /// </summary>
            /// <value></value>
            public new object this[object key]
            {
                get
                {
                    return key == null ? null : base[key];
                }
            }
        }


        /// <summary>
        /// Allows to cache all fields composing a type to be cloned. This permits to perform reflection only once
        /// for a given type to be cloned.
        /// </summary>
        private static readonly Dictionary<Type, List<FieldInfo>> _fieldsCache = new Dictionary<Type, List<FieldInfo>>();


        /// <summary>
        /// Creates a copy of the object.
        /// </summary>
        /// <param name="instance">The object to be copied.</param>
        /// <returns>A deep copy of the object.</returns>
        public static object DeepCopy(this object instance)
        {
            return instance == null ? null : instance.Clone(new VisitedGraph());
        }


        /// <summary>
        /// Creates a copy of the object.
        /// </summary>
        /// <param name="instance">The object to be copied.</param>
        /// <returns>A deep copy of the object.</returns>
        public static T DeepCopy<T>(this T instance)
            where T : class 
        {
            if (instance == null)
                return null;
            return (T)instance.Clone(new VisitedGraph());
        }


        /// <summary>
        /// Creates a copy of the object into a provided instance.
        /// </summary>
        /// <param name="instance">The object to be copied.</param>
        /// <param name="copy">The instance into which the object is copied</param>
        /// <returns>A deep copy of the object.</returns>
        public static object DeepCopy(this object instance, object copy)
        {
            if (instance == null)
                return null;
            if (copy == null)
                throw new ArgumentNullException("copy");

            Type instaceType = instance.GetType();

            if (instaceType != copy.GetType())
                throw new ArgumentException("Types of instance and copy must be the same");

            return instance.Clone(new VisitedGraph(), copy, instaceType);
        }


        /// <summary>
        /// Creates a copy of the object into a provided instance.
        /// </summary>
        /// <param name="instance">The object to be copied.</param>
        /// <param name="copy">The instance into which the object is copied</param>
        /// <returns>A deep copy of the object.</returns>
        public static object DeepCopy<T>(this T instance, T copy)
            where T : class
        {
            if (instance == null)
                return null;
            if (copy == null)
                throw new ArgumentNullException("copy");

            return instance.Clone(new VisitedGraph(), copy, typeof(T));
        }

        /// <summary>
        /// Creates a deep copy of an object using the supplied dictionary of visited objects as 
        /// a source of objects already encountered in the copy traversal. The dictionary of visited 
        /// objects is used for holding objects that have already been copied, to avoid erroneous 
        /// duplication of parts of the object graph.
        /// </summary>
        /// <param name="instance">The object to be copied.</param>
        /// <param name="visited">The graph of objects visited so far.</param>
        /// <returns></returns>
        private static object Clone(this object instance, VisitedGraph visited)
        {
            if (instance == null)
                return null;

            Type instanceType = instance.GetType();

            if (instanceType.IsValueType || instanceType == typeof(string))
                return instance; // Value types and strings are immutable

            if (instanceType.IsArray)
            {
                int length = ((Array)instance).Length;
                Array copied = (Array)Activator.CreateInstance(instanceType, length);
                visited.Add(instance, copied);
                for (int i = 0; i < length; ++i)
                    copied.SetValue(((Array)instance).GetValue(i).Clone(visited), i);
                return copied;
            }

            return instance.Clone(visited, NewInstance(instanceType), instanceType);
        }


        /// <summary>
        /// Creates a deep copy of an object into an uninitialized instance, using the supplied dictionary
        /// of visited objects as a source of objects already encountered in the copy traversal. The
        /// dictionary of visited objects is used for holding objects that have already been copied, to
        /// avoid erroneous duplication of parts of the object graph.
        /// </summary>
        /// <param name="instance">The object to be copied.</param>
        /// <param name="visited">The graph of objects visited so far.</param>
        /// <param name="copy">The unitialized instance in which to clone the object</param>
        /// <param name="instanceType">The <see cref="System.Type"/> of the object to be cloned</param>
        /// <returns></returns>
        private static object Clone(this object instance, VisitedGraph visited, object copy, Type instanceType)
        {
            if (visited.ContainsKey(instance))
                return visited[instance];

            visited.Add(instance, copy);

            foreach (FieldInfo field in FieldsList(instanceType))
            {
                object value = field.GetValue(instance);
                if (visited.ContainsKey(value))
                    field.SetValue(copy, visited[value]);
                else
                    field.SetValue(copy, value.Clone(visited));
            }

            return copy;
        }


        /// <summary>
        /// Get the list of fields composing a given <see cref="System.Type"/>.
        /// </summary>
        /// <param name="type">The <see cref="System.Type"/> for which the field list is requested.</param>
        /// <returns>The list of fields composing the <see cref="System.Type"/></returns>
        private static IEnumerable<FieldInfo> FieldsList(Type type)
        {
            List<FieldInfo> fields;

            if (!_fieldsCache.TryGetValue(type, out fields))
            {
                fields = GetFields(type);
                _fieldsCache[type] = fields;
            }

            return fields;
        }


        /// <summary>
        /// Get the list of fields composing a given <see cref="System.Type"/> through reflection.
        /// </summary>
        /// <param name="type">The <see cref="System.Type"/> for which the field list is requested.</param>
        /// <returns>The list of fields composing the <see cref="System.Type"/></returns>
        private static List<FieldInfo> GetFields(Type type)
        {
            List<FieldInfo> fields = new List<FieldInfo>();

            Type t = type;

            while (t != null)
            {
                fields.AddRange(t.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance));
                t = t.BaseType;
            }

            return fields;
        }


        /// <summary>
        /// Create a new uninitialized instance of a given <see cref="System.Type"/>.
        /// </summary>
        /// <param name="instanceType">The <see cref="System.Type"/> of the new instance to create.</param>
        /// <returns>A new unitialized object of <see cref="System.Type"/> <paramref name="instanceType"/></returns>
        private static object NewInstance(Type instanceType)
        {
            try
            {
                return FormatterServices.GetUninitializedObject(instanceType);
            }
            catch
            {
                throw new ArgumentException(string.Format("Object of type {0} cannot be cloned because an uninitialized object could not be created.", instanceType.FullName));
            }
        }
    }
}
